﻿/* Copyright (c) 2010 EPiServer AB
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using EPiServer.Web;
using System.IO;
using System.Web;
using EPiServer.Web.Hosting;
using System.Globalization;
using log4net;
using EPiServer.Security;
using System.Diagnostics;
using System.Security.Principal;
using EPiServer.Core;

namespace EPiServer.CdnSupport
{
    /// <summary>
    /// Handles the actual rewriting and validation of urls to a CDN
    /// </summary>
    public class Cdn
    {
        private HashSet<string>     _validExtensions = new HashSet<string>();
        private string              _cdnUrl;
        private bool                _cdnEnableTimestamp;
        private bool                _allowPageFilesRewrite;
        private string              _pageFilesVirtualPath;

        private static readonly ILog _log = LogManager.GetLogger(typeof(Cdn));
        private static IPrincipal _anonymousPrincipal = new GenericPrincipal(new GenericIdentity(String.Empty), null);

        #region Public props

        public HashSet<string> ValidExtensions
        {
            get { return _validExtensions; }
            set { _validExtensions = value; }
        }

        public string CdnUrl
        {
            get { return _cdnUrl; }
            set { _cdnUrl = value; }
        }

        public bool EnableTimestamp
        {
            get { return _cdnEnableTimestamp; }
            set { _cdnEnableTimestamp = value; }
        }

        #endregion

        public Cdn(string cdnUrl, bool cdnTimestamp, string validExtensions) : this(cdnUrl,cdnTimestamp,validExtensions,true,null)
        {

        }

        public Cdn(string cdnUrl, bool cdnTimestamp, string validExtensions, bool allowPageFilesRewrite, string pageFilesVirtualPath)
        {
            if (validExtensions == null)
                throw new ArgumentNullException("validExtensions");
            if (String.IsNullOrEmpty(cdnUrl))
                throw new ArgumentNullException("cdnUrl");
            if (!allowPageFilesRewrite && String.IsNullOrEmpty(pageFilesVirtualPath))
                throw new ArgumentException("pageFilesVirtualPath cannot be empty when allowPageFilesRewrite is set", "pageFilesVirtualPath");

            _cdnUrl = cdnUrl;
            _cdnEnableTimestamp = cdnTimestamp;
            _validExtensions = validExtensions=="*" ? null : new HashSet<string>(validExtensions.Split(','));
            _allowPageFilesRewrite = allowPageFilesRewrite;
            _pageFilesVirtualPath = pageFilesVirtualPath;
        }

        /// <summary>
        /// Rewrites an URL to a CDN, assumes that validation has already been completed
        /// </summary>
        /// <param name="url"></param>
        /// <param name="mapPath"></param>
        /// <returns></returns>
        public Uri RewriteUrl(UrlBuilder url,Func<string,string> mapPath)
        {
            if (_cdnEnableTimestamp)
            {
                DateTime changed = ResolveChangedDate(url, mapPath);
                if (changed != DateTime.MinValue)
                    return new System.Uri(_cdnUrl + "/cdn-" + changed.ToFileTime().ToString("x8", CultureInfo.InvariantCulture) + url.Path);
                else
                    return url.Uri;
            }
            else
            {
                return new System.Uri(_cdnUrl + url.Path);
            }
        }

        /// <summary>
        /// Check if an URL should be rewritten to a CDN based on current configuration
        /// </summary>
        /// <param name="e"></param>
        /// <returns>True if URL should be rewritten</returns>
        public bool CanRewriteToCdn(UrlRewriteEventArgs e)
        {
            if (
                    !PageReference.IsNullOrEmpty(e.Internal as PageReference) ||
                    e.Url.HasAuthorityPart ||
                    !e.Url.Path.StartsWith("/") ||
                    !String.IsNullOrEmpty(e.Url.Query) ||
                    (!_allowPageFilesRewrite && e.Url.Path.StartsWith(_pageFilesVirtualPath, StringComparison.InvariantCultureIgnoreCase))
                )
            {
                return false;
            }

            string extension = Path.GetExtension(e.Url.Path);
            if (String.IsNullOrEmpty(extension))
                return false;

            if (_validExtensions!=null)
            {
                if (!_validExtensions.Contains(extension.ToLower()))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Resolved the changed date from a file path (supports for VPP and classic files)
        /// </summary>
        /// <param name="url">The URL to the file</param>
        /// <param name="mapPath">A method that handles mapping of physical paths (non-VPP)</param>
        /// <returns>The changed date or DateTime.MinValue if method could not resolve the date</returns>
        public DateTime ResolveChangedDate(UrlBuilder url, Func<string,string> mapPath)
        {
            string decodedPath = HttpUtility.UrlDecode(url.Path);
            VirtualFileEx virtualFile = null;

            try
            {
                // Both GetFile AND HasAnonymousAccess could throw and UnauthorizedAccessException so we must
                // make sure we are inside this catch statement that takes care of this
                virtualFile = GenericHostingEnvironment.VirtualPathProvider.GetFile(decodedPath) as VirtualFileEx;
                if (virtualFile != null)
                {
                    return HasAnonymousAccess(virtualFile) ? virtualFile.Changed : DateTime.MinValue;
                }
            }
            catch (UnauthorizedAccessException e)
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Error '{0}' while resolving date for vpp '{1}'", e.Message, decodedPath);
                }
                return DateTime.MinValue;
            }
            catch (Exception e)
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Error '{0}' while resolving date for vpp '{1}'", e.Message, decodedPath);
                }
                Debug.Fail("Unknown error calling VPP");
                //Unknown error, lets still check if its a physical file so don't return here
            }

            string localPath;
            try
            {
                //This is normally connected to Server.MapPath
                localPath = mapPath(decodedPath);
                FileInfo f = new FileInfo(localPath);
                if (!f.Exists)
                    return DateTime.MinValue;
                return f.LastWriteTime;
            }
            catch (Exception e)
            {
                if (_log.IsDebugEnabled)
                {
                    _log.DebugFormat("Error '{0}' while resolving date for physical file '{1}'", e.Message, decodedPath);
                }
                return DateTime.MinValue;
            }
        }

        /// <summary>
        /// Check to see that Anonymous users have read access to the file
        /// </summary>
        /// <param name="virtualFile">The file to check</param>
        /// <exception cref="System.UnauthorizedAccessException"></exception>
        /// <returns>True if the user has access or True if no security implementation can be found</returns>
        /// <remarks>This method could throw an exception if the current user does not have access to the Parent directory for some reason</remarks>
        private static bool HasAnonymousAccess(VirtualFileEx virtualFile)
        {
            UnifiedFile uf = virtualFile as UnifiedFile;
            if (uf != null)
            {
                AccessControlList acl = uf.Parent.ACL;
                if (acl!=null)
                {
                    return acl.HasAccess(_anonymousPrincipal, AccessLevel.Read);
                }
            }
            return true;
        }
    }
}
